﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace ObservableExpressions
{
    public interface IQuery<T>
    {
        Expression Expression { get; }
        IQueryProvider Provider { get; }
        T Execute();
    }

    public interface IQueryProvider
    {
        IQuery<T> CreateQuery<T>(Expression expression);
    }

    public static class Query
    {
        public static IQuery<IEnumerable<TResult>> Select<TSource, TResult>(this IQuery<IEnumerable<TSource>> source, Expression<Func<TSource, TResult>> selector)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (source.Provider == null) throw new ArgumentException("source");

            return source.Provider.CreateQuery<IEnumerable<TResult>>(Expression.Call((MethodInfo)MethodBase.GetCurrentMethod(), 
                Expression.Constant(source, typeof(IQuery<IEnumerable<TSource>>)), selector));
        }

        public static IQuery<double> Avg(this IQuery<IEnumerable<int>> source)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (source.Provider == null) throw new ArgumentException("source");

            return source.Provider.CreateQuery<double>(Expression.Call((MethodInfo)MethodBase.GetCurrentMethod(),
                Expression.Constant(source, typeof(IQuery<IEnumerable<int>>))));
        }
    }

    public class QueryableAdapter<T> : IQuery<IEnumerable<T>>, IQueryProvider
    {
        private IQueryable<T> queryable;

        public QueryableAdapter(IQueryable<T> queryable)
        {
            this.queryable = queryable;
        }

        public Expression Expression
        {
            get { return queryable.Expression; }
        }

        IQueryProvider IQuery<IEnumerable<T>>.Provider
        {
            get { return this; }
        }

        public IEnumerable<T> Execute()
        {
            return queryable;
        }

        public IQuery<TNew> CreateQuery<TNew>(Expression expression)
        {
            var type = typeof(TNew);
            if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(IEnumerable<>))
            {

                throw new ArgumentOutOfRangeException("T");
            }
            var elementType = type.GetGenericArguments()[0];
            var method = _CreateQueryAdapter.MakeGenericMethod(elementType);
            return method.Invoke(null, new object[] { expression, queryable.Provider }) as IQuery<TNew>;
        }

        private static readonly MethodInfo _CreateQueryAdapter = typeof(QueryableAdapter<T>).GetMethod("CreateQueryAdapter", BindingFlags.NonPublic | BindingFlags.Instance);

        private static QueryableAdapter<TNew> CreateQueryAdapter<TNew>(Expression expression, System.Linq.IQueryProvider provider)
        {
            return new QueryableAdapter<TNew>(provider.CreateQuery<TNew>(expression));
        }
    }


}
